package com.fernandomartines.dbcheck.graph;

import com.fernandomartines.dbcheck.graph.element.ModelGraphAssociation;
import com.fernandomartines.dbcheck.graph.element.ModelGraphAssociationPoint;
import com.fernandomartines.dbcheck.graph.element.ModelGraphAttribute;
import com.fernandomartines.dbcheck.graph.element.ModelGraphConstraint;
import com.fernandomartines.dbcheck.graph.element.ModelGraphEntity;
import com.fernandomartines.dbcheck.graph.element.ModelGraphOperation;
import com.fernandomartines.dbcheck.graph.element.ModelGraphParameter;
import com.fernandomartines.dbcheck.graph.element.ModelGraphQualifier;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

public class ModelGraphParser {

    private Document doc;
    private ModelGraph graph;

    private PrintStream logger;

    private Document loadXMLDocument(String src, boolean validate) throws IOException {

        File xmlFile = new File(src);

        if (!xmlFile.exists())
            throw new FileNotFoundException("XML File not found.");

        String err = null;

        try {

            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setValidating(validate);
            DocumentBuilder db = dbf.newDocumentBuilder();
            Document doc = db.parse(xmlFile);

            return doc;

        } catch (ParserConfigurationException pce) {
            err = pce.toString();
        } catch (SAXParseException spe) {
            StringBuffer sb = new StringBuffer(spe.toString());
            sb.append("\n Line number  : " + spe.getLineNumber());
            sb.append("\n Column number: " + spe.getColumnNumber());
            sb.append("\n Public ID    : " + spe.getPublicId());
            sb.append("\n System ID    : " + spe.getSystemId() + "\n");
            err = sb.toString();
        } catch (SAXException se) {
            err = se.toString();
            if (se.getException() != null) {
                err += " caused by: " + se.getException().toString();
            }
        } catch (IOException ie) {
            err = ie.toString();
        }

        return null;
    }

    public ModelGraph buildGraphFromXMI(String xmiFile, PrintStream logger) throws ModelGraphException {
        this.logger = logger;
        try {
            // create xmi document
            doc = loadXMLDocument(xmiFile, false);
        } catch (IOException e) {
            throw new ModelGraphException(e.getMessage());
        }

        if (doc == null) {
            throw new ModelGraphException("XMI Document is null.");
        }

        printXMIWelcome();
        printXMIHeaderInfo();

        graph = buildGraph();

        ModelGraphPrinter.print(graph);

        return graph;
    }

    private ModelGraph buildGraph() {

        logger.println("\n#PARSER: -----------------------------------------------------------");

        // model graph which will be built
        graph = new ModelGraph();

        String  classId = null, className = null,
                associationId = null, associationName = null,
                attributeId = null, attributeName = null, attributeType = null,
                operationId = null, operationName = null, operationParameters = "", operationReturn = "",
                paramName = null, paramKind = null, paramType = null;

        // process
        Attr attr = null;
        logger.println("\nCLASSES: ");
        // classes
        NodeList list = doc.getElementsByTagName("UML:Class");
        if (list == null)
            return null;
        for (int i = 0; i < list.getLength(); i++) {
            Element element = (Element) list.item(i);
            if ((attr = element.getAttributeNode("name")) != null && element.getChildNodes().getLength() > 0) {
                classId   = element.getAttributeNode("xmi.id").getValue();
                className = attr.getValue();
                logger.printf("\nCLASS: --> %s (%s)\n", className, classId);

                // add entity
                addEntity(classId, className);

                // attributes
                NodeList attrNodeList = element.getElementsByTagName("UML:Attribute");
                for (int j = 0; j < attrNodeList.getLength(); j++) {
                    Element attributeElement = (Element) attrNodeList.item(j);
                    Attr idAttr   = attributeElement.getAttributeNode("xmi.id");
                    Attr nameAttr = attributeElement.getAttributeNode("name");
                    Attr visiAttr = attributeElement.getAttributeNode("visibility");
                    String visiAttrValue = visiAttr.getValue().toLowerCase();
                    String visiString = "private"  .equals(visiAttrValue) ? "-" :
                                        "public"   .equals(visiAttrValue) ? "+" : 
                                        "protected".equals(visiAttrValue) ? "#" : ">";
                    logger.printf(" ATTR:   %s %s", visiString, nameAttr.getValue());
                    attributeId   = idAttr.getValue();
                    attributeName = nameAttr.getValue();

                    // type
                    NodeList typeNodeList = attributeElement.getElementsByTagName("UML2:TypedElement.type");
                    for (int it = 0; it < typeNodeList.getLength(); it++) {
                        Element typeElement = (Element) typeNodeList.item(it);
                        for (int jt = 0; jt < typeElement.getChildNodes().getLength(); jt++) {
                            Node typeNode = typeElement.getChildNodes().item(jt);
                            if (typeNode.getNodeName().contains("UML:Class")) {
                                Element lookupElement = (Element) typeNode;
                                Attr xmiIdRefAttr = lookupElement.getAttributeNode("xmi.idref");
                                String xmiIdRef = xmiIdRefAttr.getValue();
                                // lookup type
                                NodeList classNodeList = doc.getElementsByTagName("UML:Class");
                                for (int il = 0; il < classNodeList.getLength(); il++) {
                                    Element lookupClassElement = (Element) classNodeList.item(il);
                                    Attr xmiIdRefLookupAttr = null, classNameLookupAttr = null;
                                    if ((xmiIdRefLookupAttr = lookupClassElement.getAttributeNode("xmi.id")) != null && (classNameLookupAttr = lookupClassElement.getAttributeNode("name")) != null) {
                                        String xmiIdRefLookup = xmiIdRefLookupAttr.getValue();
                                        if (xmiIdRef.equals(xmiIdRefLookup)) {
                                            attributeType = classNameLookupAttr.getValue();
                                            logger.printf(": %s", attributeType);
                                        }
                                    }
                                }
                            } else if (typeNode.getNodeName().contains("UML:DataType")) {
                                Element lookupElement = (Element) typeNode;
                                Attr xmiIdRefAttr = lookupElement.getAttributeNode("xmi.idref");
                                String xmiIdRef = xmiIdRefAttr.getValue();
                                // lookup type
                                NodeList dataTypeNodeList = doc.getElementsByTagName("UML:DataType");
                                for (int il = 0; il < dataTypeNodeList.getLength(); il++) {
                                    Element lookupDataTypeElement = (Element) dataTypeNodeList.item(il);
                                    Attr xmiIdRefLookupAttr = null, dataTypeNameLookupAttr = null;
                                    if ((xmiIdRefLookupAttr = lookupDataTypeElement.getAttributeNode("xmi.id")) != null && (dataTypeNameLookupAttr = lookupDataTypeElement.getAttributeNode("name")) != null) {
                                        String xmiIdRefLookup = xmiIdRefLookupAttr.getValue();
                                        if (xmiIdRef.equals(xmiIdRefLookup)) {
                                            attributeType = dataTypeNameLookupAttr.getValue();
                                            logger.printf(": %s", attributeType);
                                        }
                                    }
                                }
                            }
                        }
                    }
                    logger.println();
                    
                    // add attribute
                    addAttribute(className, attributeId, attributeName, attributeType);
                }

                // operations
                NodeList operNodeList = element.getElementsByTagName("UML:Operation");
                for (int j = 0; j < operNodeList.getLength(); j++) {
                    Element operationElement = (Element) operNodeList.item(j);
                    Attr idAttr   = operationElement.getAttributeNode("xmi.id");
                    Attr nameAttr = operationElement.getAttributeNode("name");
                    Attr visiAttr = operationElement.getAttributeNode("visibility");
                    if (nameAttr == null || visiAttr == null)
                        continue;
                    String visiOperValue = visiAttr.getValue().toLowerCase();
                    String visiString = "private"  .equals(visiOperValue) ? "-" :
                                        "public"   .equals(visiOperValue) ? "+" :
                                        "protected".equals(visiOperValue) ? "#" : ">";
                    logger.printf(" OPER:   %s %s(", visiString, nameAttr.getValue());
                    operationId   = idAttr.getValue();
                    operationName = nameAttr.getValue();

                    // type
                    NodeList typeNodeList = operationElement.getElementsByTagName("UML2:TypedElement.type");
                    for (int it = 0; it < typeNodeList.getLength(); it++) {
                        boolean firstElementComma = true;
                        Element typeElement = (Element) typeNodeList.item(it);
                        for (int jt = 0; jt < typeElement.getChildNodes().getLength(); jt++) {
                            Node typeNode = typeElement.getChildNodes().item(jt);
                            if ("#text".contains(typeNode.getNodeName()))
                                continue;
                            // parent node
                            Node typeParentNode = typeNode.getParentNode().getParentNode();
                            Element parameterElement = (Element) typeParentNode;
                            Attr parameterKindAttr = null, parameterNameAttr = null;
                            if ((parameterNameAttr = parameterElement.getAttributeNode("name")) != null && (parameterKindAttr = parameterElement.getAttributeNode("kind")) != null) {
                                paramName = parameterNameAttr.getValue();
                                paramKind = parameterKindAttr.getValue();
                                if ("in".equals(paramKind)) {
                                    if (!firstElementComma) logger.printf(", ");
                                    logger.printf("%s", paramName);
                                    firstElementComma = false;
                                } else if ("return".equals(paramKind)) {
                                    logger.printf(")");
                                }
                            }

                            if (typeNode.getNodeName().contains("UML:Class")) {
                                Element lookupElement = (Element) typeNode;
                                Attr xmiIdRefAttr = lookupElement.getAttributeNode("xmi.idref");
                                String xmiIdRef = xmiIdRefAttr.getValue();
                                // lookup type
                                NodeList classNodeList = doc.getElementsByTagName("UML:Class");
                                for (int il = 0; il < classNodeList.getLength(); il++) {
                                    Element lookupClassElement = (Element) classNodeList.item(il);
                                    Attr xmiIdRefLookupAttr = null, classNameLookupAttr = null;
                                    if ((xmiIdRefLookupAttr = lookupClassElement.getAttributeNode("xmi.id")) != null && (classNameLookupAttr = lookupClassElement.getAttributeNode("name")) != null) {
                                        String xmiIdRefLookup = xmiIdRefLookupAttr.getValue();
                                        if (xmiIdRef.equals(xmiIdRefLookup)) {
                                            paramType = classNameLookupAttr.getValue();
                                            logger.printf(": %s", paramType);
                                        }
                                    }
                                }
                            } else if (typeNode.getNodeName().contains("UML:DataType")) {
                                Element lookupElement = (Element) typeNode;
                                Attr xmiIdRefAttr = lookupElement.getAttributeNode("xmi.idref");
                                String xmiIdRef = xmiIdRefAttr.getValue();
                                // lookup type
                                NodeList dataTypeNodeList = doc.getElementsByTagName("UML:DataType");
                                for (int il = 0; il < dataTypeNodeList.getLength(); il++) {
                                    Element lookupDataTypeElement = (Element) dataTypeNodeList.item(il);
                                    Attr xmiIdRefLookupAttr = null, dataTypeNameLookupAttr = null;
                                    if ((xmiIdRefLookupAttr = lookupDataTypeElement.getAttributeNode("xmi.id")) != null && (dataTypeNameLookupAttr = lookupDataTypeElement.getAttributeNode("name")) != null) {
                                        String xmiIdRefLookup = xmiIdRefLookupAttr.getValue();
                                        if (xmiIdRef.equals(xmiIdRefLookup)) {
                                            paramType = dataTypeNameLookupAttr.getValue();
                                            logger.printf(": %s", paramType);
                                        }
                                    }
                                }
                            }
                            if ("in".equals(paramKind))
                                operationParameters += "," + paramName + ":" + paramType;
                            else if ("return".equals(paramKind))
                                operationReturn = paramType;
                        }
                    }
                    logger.println();

                    // add operation
                    addOperation(className, operationId, operationName, operationParameters, operationReturn);
                }
            }
        }

        // associations
        int classRefIndex = 0;
        attr = null;

        String[] classesId = new String[2];
        String[] roleNames = new String[2];
        String[][] multiply = new String[2][2];

        logger.println("\nASSOCIATIONS: \n");
        list = doc.getElementsByTagName("UML:Association");
        if (list == null)
            return null;

        for (int i = 0; i < list.getLength(); i++) {
            Element element = (Element) list.item(i);
            if ((attr = element.getAttributeNode("xmi.id")) != null && element.getChildNodes().getLength() > 0) {
                associationId   = element.getAttributeNode("xmi.id").getValue();
                associationName = element.getAttributeNode("name") != null ? element.getAttributeNode("name").getValue() : "NoName";
                logger.printf("ASSOC: --> %s = ", associationName);

                // association end
                classRefIndex = 0;
                NodeList assocNodeList = element.getElementsByTagName("UML:AssociationEnd");
                for (int j = 0; j < assocNodeList.getLength(); j++) {
                    Element assocElement = (Element) assocNodeList.item(j);
                    String nameAssocEnd = assocElement.getAttributeNode("name") != null ? assocElement.getAttributeNode("name").getValue() : "NoRoleName";

                    NodeList multiplyNodeList = assocElement.getElementsByTagName("UML:MultiplicityRange");
                    Element multiplyRangeElement = (Element) multiplyNodeList.item(0);
                    String lowerMultiply = multiplyRangeElement.getAttributeNode("lower").getValue();
                    String upperMultiply = multiplyRangeElement.getAttributeNode("upper").getValue();

                    NodeList participantNodeList = assocElement.getElementsByTagName("UML:AssociationEnd.participant");
                    Node participantNode = participantNodeList.item(0);
                    for (int ip = 0; ip < participantNode.getChildNodes().getLength(); ip++) {
                        Node particClassNode = participantNode.getChildNodes().item(ip);
                        if (!"#text".equals(particClassNode.getNodeName())) {
                            Element particClassElement = (Element) particClassNode;
                            String xmiIdRef = particClassElement.getAttributeNode("xmi.idref").getValue();
                            classId = xmiIdRef;
                        }
                    }
                    classesId[classRefIndex] = classId;
                    roleNames[classRefIndex] = nameAssocEnd;
                    multiply [classRefIndex][0] = lowerMultiply;
                    multiply [classRefIndex][1] = upperMultiply;
                    classRefIndex++;
                    classRefIndex = classRefIndex != 0 ? 1 : 0;
                }
                logger.printf("%s (%s) {%s..%s} <---> %s (%s) {%s..%s}\n",
                        classesId[0], roleNames[0], multiply[0][0], multiply[0][1],
                        classesId[1], roleNames[1], multiply[1][0], multiply[1][1]
                        );
                
                // adding association
                addAssociation(associationId, associationName,
                               classesId[0], roleNames[0], multiply[0][0], multiply[0][1],
                               classesId[1], roleNames[1], multiply[1][0], multiply[1][1]);
            }
        }

        return graph;
    }

    private Node getChildNode(NodeList list) {
        Node node = null;
        boolean searchingNode = true;
        int searchCounter = 1000;
        int ix = 0;
        while (searchingNode && searchCounter-- > 0) {
            node = list.item(ix++);
            if (node == null)
                return null;
            searchingNode = node.getChildNodes().getLength() <= 0;
        }
        if (searchCounter <= 0)
            return null;
        return node;
    }

    private String getNodeContent(String nodeName) {
        NodeList list = doc.getElementsByTagName(nodeName);
        if (list == null)
            return null;
        Element element = (Element) list.item(0);
        return element.getTextContent();
    }

    // parse-related operations

    public static final String QualifierTokens  = "<>";
    public static final String ConstraintTokens = "{}";
    private final char ReplaceToken = '\u0255';

    private boolean isModelGraphElement(String text, String tokens) {
        return text != null && text.length() >= 2
            && tokens != null && tokens.length() == 2
            && text.charAt(0) == tokens.charAt(0)
            && text.charAt(text.length() - 1) == tokens.charAt(1)
             ? true : false;
    }
    private boolean isModelGraphQualifier(String s) {
        return isModelGraphElement(s, QualifierTokens);
    }

    private boolean isModelGraphConstraint(String s) {
        return isModelGraphElement(s, ConstraintTokens);
    }

    // parse operations

    private String prepareModelGraphElementString(String text) {
        String[] reservedTokens = {QualifierTokens, ConstraintTokens};
        char space = ' ', refChar = ReplaceToken;
        char replaceChar = space;
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < text.length(); i++) {
            for (String s: reservedTokens) {
                char reservedStartChar = s.charAt(0);
                char reservedEndChar = s.charAt(1);
                if (text.charAt(i) == reservedStartChar) {
                    replaceChar = refChar;
                } else if (text.charAt(i) == reservedEndChar) {
                    replaceChar = space;
                }
            }
            if (text.charAt(i) == space)
                sb.append(String.valueOf(replaceChar));
            else
                sb.append(String.valueOf(text.charAt(i)));
        }

        return sb.toString();
    }

    private String[] parseModelGraphElements(String rawElements) {
        String s = prepareModelGraphElementString(rawElements);
        String[] arr = s.split(" ");
        return arr != null ? arr : new String[0];
    }

    private ModelGraphQualifier parseQualifier(String text) {
        ModelGraphQualifier qualifier = new ModelGraphQualifier();
        for (char c: QualifierTokens.toCharArray()) {
            text = text.replaceAll(String.valueOf(c), "");
            text = text.replaceAll(String.valueOf(ReplaceToken), " ");
        }
        text = text.trim();

        // assign values
        qualifier.setId(text);
        qualifier.setName(text);

        return qualifier;
    }

    private ModelGraphConstraint parseConstraint(String text) {
        ModelGraphConstraint constraint = new ModelGraphConstraint();
        for (char c: ConstraintTokens.toCharArray()) {
            text = text.replaceAll("\\" + String.valueOf(c), "");
            text = text.replaceAll(String.valueOf(ReplaceToken), " ");
        }
        text = text.trim();
        String name = null, value = null;
        name = text; value = null;

        String[] arr = text.split(":");
        if (arr != null && arr.length > 1) {
            name = arr[0].trim();
            value = arr[1].trim();
        }

        // assign values
        constraint.setName(name);
        constraint.setValue(value);

        return constraint;
    }

    private ModelGraphAttribute parseAttribute(String attributeId, String attributeName, String attributeType) {
        String[] elements = parseModelGraphElements(attributeType);

        ModelGraphAttribute attribute = new ModelGraphAttribute(attributeId, attributeName);
        // parse type, qualifiers and constraints
        int elementCount = 0;
        for (String element: elements) {
            // the first element must be the attribute type
            if (elementCount++ == 0) {
                attribute.setType(element);
                continue;
            }
            // check if element is a qualifier
            if (isModelGraphQualifier(element)) {
                ModelGraphQualifier qualifier = parseQualifier(element);
                attribute.getQualifiers().add(qualifier);
            }
            // else, check if element is a constraint
            else if (isModelGraphConstraint(element)) {
                ModelGraphConstraint constraint = parseConstraint(element);
                attribute.getConstraints().add(constraint);
            }
            elementCount++;
        }
        return attribute;
    }

    private ModelGraphOperation parseOperation(String operationId, String operationName, String operationParameters, String operationReturn) {

        String[] elements = parseModelGraphElements(operationReturn);

        ModelGraphOperation operation = new ModelGraphOperation(operationId, operationName);
        // parse result, qualifiers and constraints
        int elementCount = 0;
        for (String element: elements) {
            // the first element must be the attribute type
            if (elementCount++ == 0) {
                operation.setResult(element);
                continue;
            }
            // check if element is a qualifier
            if (isModelGraphQualifier(element)) {
                ModelGraphQualifier qualifier = parseQualifier(element);
                operation.getQualifiers().add(qualifier);
            }
            // else, check if element is a constraint
            else if (isModelGraphConstraint(element)) {
                ModelGraphConstraint constraint = parseConstraint(element);
                operation.getConstraints().add(constraint);
            }
        }
        // parse parameters
        String[] parameters = null;
        if (operationParameters.contains(",")) {
            parameters = operationParameters.split(",");
        } else {
            parameters = new String[] {operationParameters};
        }
        for (String p: parameters) {
            String[] param = p.split(":");
            if (param != null && param.length == 2) {
                ModelGraphParameter parameter = new ModelGraphParameter();
                parameter.setName(param[0].trim());
                parameter.setType(param[1].trim());
                operation.getParameters().add(parameter);
            }
        }
        return operation;
    }

    private ModelGraphAssociationPoint parseAssociationPoint(ModelGraphEntity entity, String roleName, String multiplyMin, String multiplyMax) {
        ModelGraphAssociationPoint assocationPoint = new ModelGraphAssociationPoint();
        assocationPoint.setEntity(entity);
        assocationPoint.setId(roleName);
        assocationPoint.setRoleName(roleName);
        try {
            assocationPoint.setMin(Integer.parseInt(multiplyMin));
            assocationPoint.setMax(Integer.parseInt(multiplyMax));
        } catch (Exception e) {
            assocationPoint.setMin(-99);
            assocationPoint.setMax(-99);
        }
        return assocationPoint;
    }

    // add operations

    private void addEntity(String entityId, String entityName) {
        ModelGraphEntity entity = new ModelGraphEntity(entityId, entityName);
        graph.getEntities().add(entity);
    }

    private void addAttribute(String entityName, String attributeId, String attributeName, String attributeType) {
        ModelGraphEntity entity = graph.getEntities().getEntityByName(entityName);
        assert entity != null;
        ModelGraphAttribute attribute = parseAttribute(attributeId, attributeName, attributeType);
        entity.getAttributes().add(attribute);
    }

    private void addOperation(String entityName, String operationId, String operationName, String operationParameters, String operationReturn) {
        ModelGraphEntity entity = graph.getEntities().getEntityByName(entityName);
        assert entity != null;
        ModelGraphOperation operation = parseOperation(operationId, operationName, operationParameters, operationReturn);
        entity.getOperations().add(operation);
    }

    private void addAssociation(String associationId, String associationName,
            String entityId1, String roleName1, String multiplyMin1, String multiplyMax1,
            String entityId2, String roleName2, String multiplyMin2, String multiplyMax2)
    {
        ModelGraphEntity entity1 = graph.getEntities().getEntityById(entityId1);
        assert entity1 != null;
        ModelGraphEntity entity2 = graph.getEntities().getEntityById(entityId2);
        assert entity2 != null;

        ModelGraphAssociationPoint associationPoint1 = parseAssociationPoint(entity1, roleName1, multiplyMin1, multiplyMax1);
        ModelGraphAssociationPoint associationPoint2 = parseAssociationPoint(entity2, roleName2, multiplyMin2, multiplyMax2);

        ModelGraphAssociation association = new ModelGraphAssociation(associationId, associationName, associationPoint1, associationPoint2);

        graph.getAssociations().add(association);
    }

    private void printXMIWelcome() {
        logger.printf("XMIParser ======================================== version %s ===\n", ModelGraphVersion.VERSION);
    }

    private void printXMIHeaderInfo() {
        logger.printf("Exporter  .......: %s\n", getNodeContent("XMI.exporter"));
        logger.printf("Exporter  Version: %s\n", getNodeContent("XMI.exporterVersion"));
        logger.printf("MetaModel Version: %s\n", getNodeContent("XMI.metaModelVersion"));

    }

}
